using System;
using System.Collections;
using System.Collections.Generic;
using System.IO;
using System.Reflection;
using System.Text;
using System.Text.RegularExpressions;

using System.Management.Automation;
using Microsoft.PowerShell.CmdletManagement.CompareCmdlet;

namespace Microsoft.PowerShell.CmdletManagement
{
    [Cmdlet(VerbsData.Compare, "PsSpecCmdlet", SupportsShouldProcess = true)]
    public class ComparePsSpecCmdletCommand : Cmdlet
    {
        [Parameter(Mandatory = false, Position = 1, ValueFromPipelineByPropertyName = true)]
        [ValidateLength(3, 101)]
        public String[] CmdletName
        {
            get
            {
                return _cmdletNames;
            }
            set
            {
                _cmdletNames = value;
            }
        }
        private string[] _cmdletNames = null;

        [Parameter(Mandatory = true, Position = 0, ValueFromPipelineByPropertyName = true)]
        [ValidateLength(1, 50)]
        public String ProjectName
        {
            get
            {
                return _projectName;
            }
            set
            {
                _projectName = value;
            }
        }
        private string _projectName = null;

        [Parameter(Mandatory = true, Position = 2)]
        [ValidateLength(2, 256)]
        public String[] Path
        {
            get
            {
                return _paths;
            }
            set
            {
                _paths = value;
            }
        }
        private string[] _paths = null;

        [Parameter(Mandatory = false)]
        public String ServiceUri
        {
            get
            {
                return _serviceUri;
            }
            set
            {
                _serviceUri = value;
            }
        }
        private string _serviceUri = PsSpecCmdletUtils.Constants.ServiceUri;

        protected override void ProcessRecord()
        {
            // Verify if the provided project exists
            if(!VerifyProjectExistence()) {
                WriteError(new ErrorRecord(
                    new Exception("A project with the name ["+ProjectName+"] does not exist."),
                    "Project does not exist.",ErrorCategory.ObjectNotFound,"ProjectName"));
                return;
            }
            List<Assembly> assemblies = new List<Assembly>();
            if(!GetAssemblies(_paths, assemblies))
                // There is a problem with one of the provided assembly paths.
                // An ErrorRecord has already been written.
                return;
            if (null == _cmdletNames) {
                // Cmdlet names are not provided
                LinkedList<SpecifiedCmdletInfo> specifiedCmdletList = 
                    SpecifiedCmdletInfo.RetrieveCmdletInfoFromDatabase(ProjectName, ServiceUri);
                LinkedList<ImplementedCmdletInfo> implementedCmdletList =
                    ImplementedCmdletInfo.RetrieveImplementedCmdlets(assemblies);
                // Compare specified cmdlets agaist implemented ones
                ImplementedCmdletInfo outImplementedCmdlet = null;
                foreach(SpecifiedCmdletInfo specifiedCmdlet in specifiedCmdletList) {
                    if (ImplementedMatchExists(implementedCmdletList, specifiedCmdlet, out outImplementedCmdlet)) {
                        // The cmdlet both is specified and implemented
                        specifiedCmdlet.CompareToImplementation(outImplementedCmdlet);
                    } else {
                        // The cmdlet is specified but is not implemented.  Add a mismatch.
                        IMismatch mismatch = new CmdletMismatch(specifiedCmdlet,
                            String.Format("The cpecified cmdlet [{0}] is not implemented in [{1}].", specifiedCmdlet.CompositeName(), ComposeNames(assemblies)));
                        specifiedCmdlet.AddMismatch(mismatch);
                    }
                    // Collect all found mismatches for this specified cmdlet
                    specifiedCmdlet.AddMismatchesToList(_mismatches);
                }
                // Verify if there are any cmdlets that are not specified but implemented
                foreach(ImplementedCmdletInfo implementedCmdlet in implementedCmdletList) {
                    if(!implementedCmdlet.Specified) {
                        // The cmdlet is implemented but is not specified
                        IMismatch mismatch = new CmdletMismatch(implementedCmdlet,
                            String.Format("The implemented cmdlet [{0}] is not specified.", implementedCmdlet.CompositeName()));
                        _mismatches.Add(mismatch);
                    }
                }
            } else {
                // Cmdlet names are provided
                LinkedList<SpecifiedCmdletInfo> specifiedCmdletList;
                string[] resolvedNames = new string[]{};
                foreach(string cmdletName in _cmdletNames) {
                    // Retrieve the cmdlet info from the database
                    specifiedCmdletList = 
                        SpecifiedCmdletInfo.RetrieveCmdletInfoFromDatabase(ProjectName, ServiceUri, cmdletName);
                    if(specifiedCmdletList.Count <= 0) {
                        // The cmdlet with the provided name was not specified.
                        CompareCmdlet.CmdletInfo cmdlet = new CompareCmdlet.CmdletInfo(cmdletName);
                        IMismatch mismatch = new CmdletMismatch(cmdlet,
                            String.Format("The provided cmdlet [{0}] is not specified.", cmdletName));
                        _mismatches.Add(mismatch);
                        // Verify if the provided cmdlet is implemented
                        string escapedName = cmdletName.Replace("*",".*"); // Substitude "*" for ".*"
                        LinkedList<ImplementedCmdletInfo> implementedCmdletList =
                            ImplementedCmdletInfo.RetrieveImplementedCmdlets(assemblies, new Regex(escapedName));
                        // Generate a mismatch if there is no implemented cmdlets with the provided name
                        if(implementedCmdletList.Count <= 0) {
                            mismatch = new CmdletMismatch(cmdlet,
                                String.Format("The provided cmdlet [{0}] is not implemented.", cmdletName));
                            _mismatches.Add(mismatch);
                        }
                    } else {
                        // The provided name refers to one or several cmdlets
                        foreach(SpecifiedCmdletInfo specifiedCmdlet in specifiedCmdletList) {
                            LinkedList<ImplementedCmdletInfo> implementedCmdletList =
                                ImplementedCmdletInfo.RetrieveImplementedCmdlets(assemblies, specifiedCmdlet.Verb, specifiedCmdlet.Noun);
                            int count = implementedCmdletList.Count;
                            if(count <= 0) {
                                IMismatch mismatch = new CmdletMismatch(specifiedCmdlet,
                                    String.Format("The provided cmdlet [{0}] is not implemented.", specifiedCmdlet.CompositeName()));
                                _mismatches.Add(mismatch);
                            } else if(count > 1) {
                                IMismatch mismatch = new CmdletMismatch(specifiedCmdlet,
                                    String.Format("The provided cmdlet [{0}] is implemented {1} times.", specifiedCmdlet.CompositeName(), count));
                                _mismatches.Add(mismatch);
                            } else {
                                specifiedCmdlet.CompareToImplementation(implementedCmdletList.First.Value);
                                specifiedCmdlet.AddMismatchesToList(_mismatches);
                            }
                        }
                    }
                }
            }
            // Report all found mismatches
            WriteObject(_mismatches, true);
        }

        /// <summary>
        /// All Assembly objects for loaded assemblies are cached into this table not to load the same assembly
        /// again to verify a second cmdlet in it.
        /// Used to optimize performance
        /// </summary>
        private Hashtable _assemblyCache = new Hashtable();

        /// <summary>
        ///  It is a global storage for all mismatches
        /// </summary>
        private List<IMismatch> _mismatches = new List<IMismatch>();

        private bool GetAssemblies(string[] paths, List<Assembly> assemblies) {
            Assembly assmbly = null;
            for(int i = 0; i < paths.Length; i++) {
                paths[i] = System.IO.Path.GetFullPath(paths[i]);
            }
            // Discard possible duplicate paths
            RemoveDuplicates(ref paths);

            foreach(string path in paths) {
                assmbly = GetAssembly(path);
                if (null != assmbly)
                    assemblies.Add(assmbly);
                else
                    // Faulure to load an assembly
                    return false;
            }
            return true;
        }

        /// <summary>
        /// Returnes an Assembly object referencing to file in AssemblyPath parameter.
        /// It first checks if assembly is already located and if it is not it stores it in a
        /// hashtable (AssemblyCache) not to load the same assembly again and again.
        /// </summary>
        /// <param name="AssemblyPath">Path to assembly</param>
        /// <returns>Assembly</returns>
        private Assembly GetAssembly(string assemblyPath) {
            //Verify that Assembly is not already cached
            if (_assemblyCache[assemblyPath] == null) {
                //Load the assembly and add a reference to it into assembly cache
                Assembly cmdletAssembly;
                try {
                    cmdletAssembly = Assembly.LoadFile(assemblyPath);
                } catch (Exception e) {
                    WriteError(new ErrorRecord(
                        new Exception(String.Format("Error loading assembly [{0}].\r\n", assemblyPath), e),
                        "Error loading assembly.",ErrorCategory.ObjectNotFound,"Assembly"));
                        return null;
                }

                _assemblyCache[assemblyPath] = cmdletAssembly;
                return cmdletAssembly;
            } else {
                //assembly is already cached.
                return (_assemblyCache[assemblyPath] as Assembly);
            }
        }

        private void RemoveDuplicates(ref string[] paths) {
            // Remove duplicate paths
            if(paths.Length > 1) {
                int pad = 0;
                for(int y = 1; y < paths.Length - pad; y++) {
                    // Go through the begging of the array to see if there is a match there
                    for(int b = 0; b < y && y < (paths.Length - pad); b++) {
                        if(paths[b].CompareTo(paths[y]) == 0) {
                            // A previos path and the current path are equal. Exclude the current path.
                            int t = y;
                            while(t < paths.Length -1 -pad) {
                                paths[t] = paths[t+1];
                                t++;
                            }
                            paths[t] = null;
                            pad++;
                            b = -1; // Begin verification from the begging of the array
                        }
                    }
                }
                Array.Resize<string>(ref paths, paths.Length - pad);
            }
        }

        private bool VerifyProjectExistence() {
            // Initiate the web service
            PsSpecCmdletService.CmdletService service = new PsSpecCmdletService.CmdletService();
            service.Url = ServiceUri;
            service.UseDefaultCredentials = true;

            PsSpecCmdletService.PsSpecProject[] projects = service.GetProject(ProjectName);
            if(projects == null || projects.Length <= 0)
                return false;
            return true;
        }

        private bool ImplementedMatchExists(LinkedList<ImplementedCmdletInfo> implementedCmdletList,
                                            SpecifiedCmdletInfo specifiedCmdlet,
                                            out ImplementedCmdletInfo outImplementedCmdlet) {
            foreach(ImplementedCmdletInfo implementedCmdlet in implementedCmdletList) {
                if(!implementedCmdlet.Specified && implementedCmdlet.HasName(specifiedCmdlet)) {
                    // A matching cmdlet is found
                    // Mark the matching cmdlet as being found already
                    implementedCmdlet.Specified = true;
                    outImplementedCmdlet = implementedCmdlet;
                    return true;
                }
            }
            outImplementedCmdlet = null;
            return false;
        }

        private string ComposeNames(List<Assembly> assemblies) {
            string names = "";
            bool first = true;
            foreach(Assembly asmbl in assemblies) {
                if (!first) {
                    names += ", ";
                } else
                    first = false;
                names += asmbl.Location;
            }
            return names;
        }
    }
}
